Skip to content

Java BigDecimal

在带有小数的数值计算(+-*/)中,floatdouble 类型的计算结果往往不如人意。

java
System.out.println(1.1 + 0.1); // 1.2000000000000002
System.out.println(1.1 - 0.1); // 1.0
System.out.println(1.1 * 0.1); // 0.11000000000000001
System.out.println(1.1 / 0.1); // 11.0

这是因为现在的计算机主要采用的是754 - IEEE 浮点算术标准来表示浮点数,对小数的存储只能尽量做到精确,却做不到完全精确。

《Effective Java》这本书中对 floatdouble 的使用总结的很好:

floatdouble 类型主要用于科学和工程计算。 它们执行二进制浮点运算,经过精心设计,可在很宽的范围内快速提供准确的近似值。 但是,它们不能提供准确的结果,不应在需要确切结果的地方使用。 floatdouble 类型特别不适合进行货币计算,因为不可能将 0.1(或任何其他 10 的负次方)精确地表示为 floatdouble

解决此问题的正确方法是使用 BigDecimalintlong 进行货币计算。也就是说,可以将小数转成 BigDecimal,或将小数乘以一个固定的倍率转换为 intlong,计算完成后再除以倍率转换回来。

BigDecimal 简介

BigDecimal 的全类名为 java.math.BigDecimal。根据 Java API 文档说明,BigDecimal 的特点如下:

  • 不可变的、任意精度的有符号小数
  • 提供用于算术、数值范围处理、舍入、比较、哈希和格式转换的操作
  • 完全控制舍入行为

BigDecimal 构造方法

BigDecimal 类提供了多个构造方法供我们实例化 BigDecimal,常用的是下面几种:

  • public BigDecimal(int val)int 转换为 BigDecimal
  • public BigDecimal(double val)double 转换为 BigDecimal(不建议)
  • public BigDecimal(String val)String 转换为 BigDecimal
java
// 实例化 BigDecimal
BigDecimal bigDecimal = new BigDecimal(10); // 将 int 转换为 BigDecimal
BigDecimal bigDecimal1 = new BigDecimal(10.0); // 将 double 转换为 BigDecimal(不建议)
BigDecimal bigDecimal2 = new BigDecimal("10.0"); // 将 String 转换为 BigDecimal

为什么不建议使用 BigDecimal(double val) 来实例化 BigDecimal

java
// 不建议使用 double 构造 BigDecimal
BigDecimal bigDecimal = new BigDecimal(0.1);
System.out.println(bigDecimal);

打印结果为:

0.1000000000000000055511151231257827021181583404541015625

可以看到,这比我们直接使用 double 类型还要糟糕。对此,Java API 文档做了注释:

  1. 这个构造函数的结果可能有些不可预测。您可能会假设在 Java 中写入新的 BigDecimal(0.1) 会创建一个完全等于 0.1 的 BigDecimal(一个未缩放的值 1,刻度为 1),但是它实际上等于 0.1000000000000000055511151231257827021181583404541015625。这是因为 0.1 不能精确地表示为一个双精度数(或者说,任何有限长度的二进制分数)。因此,传递给构造函数的值并不完全等于 0.1,尽管看起来是这样。

  2. 另一方面,字符串构造函数是完全可预测的:编写新的 BigDecimal("0.1") 将创建一个完全等于 0.1 的 BigDecimal,这与预期的一样。因此,通常建议优先使用字符串构造函数

  3. double 必须用作 BigDecimal 的源时,请注意此构造函数提供了精确的转换;与使用 double.tostring(double) 方法将 double 转换为字符串,然后使用 BigDecimal(String) 构造函数得到的结果不同。要得到这个结果,使用静态 valueOf(double) 方法

也就是说,必须使用 double 创建一个 BigDecimal 时,可以使用 BigDecimal.valueOf(double) 静态方法生成 BigDecimal 对象:

java
// 使用 BigDecimal.valueOf(double) 静态方法生成 BigDecimal
BigDecimal bigDecimal = BigDecimal.valueOf(0.1);
System.out.println(bigDecimal); // 0.1

BigDecimal 算术运算(加减乘除)

BigDecimal 提供了 addsubtractmultiplydivide 方法进行“加减乘除”运算操作。

java
// BigDecimal 加减乘除
BigDecimal x = new BigDecimal("1.1");
BigDecimal y = new BigDecimal("0.1");

System.out.println("x + y = " + x.add(y)); // x + y = 1.2
System.out.println("x - y = " + x.subtract(y)); // x - y = 1.0
System.out.println("x * y = " + x.multiply(y)); // x * y = 0.11
System.out.println("x / y = " + x.divide(y)); // x / y = 11

可以看到,这样的结果才是我们想要的。

注意

就像 String 一样,BigDecimal 也是不可变的类型。这意味着“加减乘除”的运算结果都会返回一个全新的 BigDecimal

BigDecimal 中的除法

对于 “10 / 3” 的结果,数学中可以表示为 “≈3.33” ,而在 Java 中会直接舍去小数部分,只取商。

java
System.out.println(10 / 3); // 3

想要保留小数部分,需要除数或被除数为小数形式:

java
System.out.println(10.0 / 3); // 3.3333333333333335

可是 floatdouble 运算结果的精度是无法预料的,那使用 BigDecimal 呢?

java
BigDecimal i = BigDecimal.valueOf(10);
BigDecimal j = BigDecimal.valueOf(3);
System.out.println("i / j = " + i.divide(j)); // java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

“10 / 3”的结果是一个无穷小数,不抛异常才怪(原来刚才计算“1.1 / 0.1”正常,完全是运气好 o(∩_∩)o 哈哈)。

BigDecimal divide(BigDecimal divisor) 方法的描述如下:

返回一个 BigDecimal,其值为(this / divisor),其首选数值范围为(this.scale() - divisor.scale());如果不能表示精确的商(由于它是无限小数),则抛出 ArithmeticException 异常。

为了解决除不尽的问题,需要使用 divide 的重载(overload)方法:

BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode)

方法参数含义为:

divisor - 除数

scale - 小数的数值范围(小数点后几位)

roundingMode - 舍入模式(八种舍入模式)

例如,要保留两位小数,且舍去(截断)后面的小数:

java
BigDecimal i = BigDecimal.valueOf(10);
BigDecimal j = BigDecimal.valueOf(3);
System.out.println("i / j = " + i.divide(j, 2, RoundingMode.DOWN)); // 3.33

BigDecimal 中的舍入模式

RoundingMode 类是一个枚举类,其中有八种枚举类型(UPDOWNCEILINGFLOORHALF_UPHALF_DOWNHALF_EVENUNNECESSARY),分别代表八种不同的舍入模式。

RoundingMode.UP

进一。以 0 为基准,舍入后的数字远离 0。

输入数字远离 0 取整
5.56
2.53
1.62
1.12
1.01
-1.0-1
-1.1-2
-1.6-2
-2.5-3
-5.5-6

总结:RoundingMode.UP 模式下,正数往大舍入,负数往小舍入。

RoundingMode.DOWN

截断。以 0 为基准,舍入后的数字靠近 0。

输入数字靠近 0 取整
5.55
2.52
1.61
1.11
1.01
-1.0-1
-1.1-1
-1.6-1
-2.5-2
-5.5-5

总结:RoundingMode.DOWN 模式下,正数往小舍入,负数往大舍入。

RoundingMode.CEILING

舍入后的数字靠近正无穷。

输入数字靠近正无穷
5.56
2.53
1.62
1.12
1.01
-1.0-1
-1.1-1
-1.6-1
-2.5-2
-5.5-5

总结:RoundingMode.CEILING 模式下,正数与负数都往大舍入。

RoundingMode.FLOOR

舍入后的数字靠近负无穷。

输入数字靠近负无穷
5.55
2.52
1.61
1.11
1.01
-1.0-1
-1.1-2
-1.6-2
-2.5-3
-5.5-6

总结:RoundingMode.FLOOR 模式下,正数与负数都往小舍入。

RoundingMode.HALF_UP

四舍五入。如果舍去位的值 >= 5,舍入模式同 RoundingMode.UP;否则同 RoundingMode.DOWN

输入数字“四舍”靠近 0,“五入”远离 0
5.56
2.53
1.62
1.11
1.01
-1.0-1
-1.1-1
-1.6-2
-2.5-3
-5.5-6

总结:RoundingMode.HALF_UP 模式下,“四舍”同 RoundingMode.DOWN,“五入”同 RoundingMode.UP

RoundingMode.HALF_DOWN

五舍六入。如果舍去位的值 >= 6,舍入模式同 RoundingMode.UP;否则同 RoundingMode.DOWN

输入数字“五舍”靠近 0,“六入”远离 0
5.55
2.52
1.62
1.11
1.01
-1.0-1
-1.1-1
-1.6-2
-2.5-2
-5.5-5

总结:RoundingMode.HALF_DOWN 模式下,“五舍”同 RoundingMode.DOWN,“六入”同 RoundingMode.UP

RoundingMode.HALF_EVEN

“银行家的舍入(Banker's Rounding)”模式。

  • 如果舍去位的值 > 5,舍入模式同 RoundingMode.UP
  • 如果舍去位的值 < 5,舍入模式同 RoundingMode.DOWN
  • 如果舍去位的值 = 5 且 5 后不为空且非全 0 时,舍入模式同 RoundingMode.UP
  • 如果舍去位的值 = 5 且 5 后为空或全 0 时:如果前位数值为奇数,舍入模式同 RoundingMode.UP;如果前位数值为偶数,舍入模式同 RoundingMode.DOWN
输入数字“银行家的舍入”
5.56
2.52
1.62
1.11
1.01
-1.0-1
-1.1-1
-1.6-2
-2.5-2
-5.5-6

总结:RoundingMode.HALF_EVEN 模式下,四舍六入五考虑,五后非空就进一,五后为空看奇偶,五前为偶应舍去,五前为奇要进一。

RoundingMode.UNNECESSARY

断言所请求的操作有确切的结果,因此不需要舍入。如果在产生不精确结果的操作上指定了这种舍入模式,则会抛出 ArithmeticException 异常。

输入数字不需要舍入
5.5throw ArithmeticException
2.5throw ArithmeticException
1.6throw ArithmeticException
1.1throw ArithmeticException
1.01
-1.0-1
-1.1throw ArithmeticException
-1.6throw ArithmeticException
-2.5throw ArithmeticException
-5.5throw ArithmeticException

总结:RoundingMode.UNNECESSARY 模式下,必须保证结果是精确的。

BigDecimal 比较(compareTo 和 equals)

对于 floatdouble 等基本的数值类型,可以通过布尔比较运算符(<==>>=!=<=)比较大小及是否相等。而对于它们的包装类型(FloatDouble 等),由于 Java 没有重载对应的运算符,所以只能通过 compareToequals 方法来比较大小及是否相等。

同样,在 BigDecimal 中,也提供了这两个方法。

java
// compareTo 和 equals
BigDecimal x = new BigDecimal(10);
BigDecimal y = new BigDecimal(3);
System.out.println(x.compareTo(y)); // 1
System.out.println(x.equals(y)); // false

compareTo 的返回值是一个 int,具体值只会是 -1、0、1 其中之一。

  • 如果 x < y,则返回 -1
  • 如果 x = y,则返回 0
  • 如果 x > y,则返回 1

通过使用 compareTo,我们可以自行组合不同的布尔运算,建议方法是:(x.compareTo(y) <op> 0),其中 <op> 是六个布尔比较运算符(<==>>=!=<=)之一。

java
// 组合不同的布尔运算(<, ==, >, >=, !=, <=)
BigDecimal x = new BigDecimal(10);
BigDecimal y = new BigDecimal(3);
System.out.println("x < y = " + (x.compareTo(y) < 0)); // x < y = false
System.out.println("x == y = " + (x.compareTo(y) == 0)); // x == y = false
System.out.println("x > y = " + (x.compareTo(y) > 0)); // x > y = true
System.out.println("x >= y = " + (x.compareTo(y) >= 0)); // x >= y = true
System.out.println("x != y = " + (x.compareTo(y) != 0)); // x != y = true
System.out.println("x <= y = " + (x.compareTo(y) <= 0)); // x <= y = false

提示

compareTo 不同的是,equals 方法只在两个 BigDecimal 对象在值和数值范围上相等时才认为它们相等(因此通过该方法进行比较时,2.0 不等于 2.00)。

Java API 文档中的描述并不完整,这句话只对通过字符串构造的 BigDecimal 对象生效。

java
// 通过字符串构造的 2.0 2.00
BigDecimal x = new BigDecimal("2.0");
BigDecimal y = new BigDecimal("2.00");
System.out.println(x.compareTo(y) == 0); // true
System.out.println(x.equals(y)); // false

// 通过数值构造的 2.0 2.00
BigDecimal xx = new BigDecimal(2.0);
BigDecimal yy = new BigDecimal(2.00);
System.out.println(xx.compareTo(yy) == 0); // true
System.out.println(xx.equals(yy)); // true

Released under the MIT License.